// Fireworks JavaScript Command // Deforms one or more selected paths to the contour of the top selected path // Install by copying to Fireworks/Configuration/Commands/ // Run in Fireworks via the Commands menu // Aaron Beall 2008-2011 - http://abeall.com // Version 1.6.4 /* BUGS - [FIXED-v1.5] sometimes the very tip seems to inverse x coord. wrongly calculated tangent/normal? -- the problem was deforming handles that are outside the bounding, the bounding box is now calculated manually based on all points and handle point positions - [FIXED-v1.3] sometimes node handles are distorted very badly, perhaps a problem with the auto-tangent routine - [FIXED-v1.3] first nodes tangents are wrong, they appear to be stuck on 0 - [FIXED-v1.5] comformity threshold caclulating points on curves is not very accurate in where it adds points, it doesnt add them on the curves. compare Add Points To Curves at 45, which works good vs Deform to Path at 45 conformity - [FIXED-v1.6] sometimes a point is not added that should be, intersetion error - [FIXED-v1.6.1] pressing cancel on input dialog continues anyway and throws error - [CHANGED-v1.6.2] copyObject/pasteObject to copyNode/pasteNode - [CHANGED-v1.6.3] changed for..in to for - [CHANGED-v1.6.4] optimized node bound calculation - [CHANGED-v1.6.4] automatically change fill rule to nonzero winding when path has only 1 contour */ /* TODO - add handles to lines before intersecting on threshold curves - [DONE-v1.2] find paths within groups - [DONE-v1.3] use straight edge smoothing, like Fisheye - provide "stretch along path[default] / repeat along path" option - provide "proportional" option, which will scale the object width based on how stretched it will be vertically - provide tapering control - provide orientation option: up(default), down, left, right - - provide offset (-+) value - provide "inside/center/outside" placement option - [DONE-v1.2] auto detect most likely horizontal/vertical orientation based on bounds aspect ratio - [DEFER-stupid idea] provide option to delete deformPath after completion - [DONE-v1.4] adaptively add points as necessary to conform to path - - [DONE-v1.4] this is really slow, because it re-applies and re-renders the path per refinement iteration, it could be optimized by working on a non-real array of nodes, and applying only after all the iterations - - [FIXED-v1.5] this is not accurate - - [DEFER-it will be very hard to control the selection, because Join puts the resulting path at the top of the layer stack] consider using dom.knifeElementsFromPoints to add the points, it will probably be a lot faster, then we can join the pieces back together. This might totally screw up the selection, though. - [DONE-v1.1] on a closed path loop all the way around - [DEFER-I think in general this is something you always want, additonally, performance has been greatly increased in v1.6] provide auto-smooth only as option to determine whether or not to perform the final smoothing/preserve-tangent of curve points - [DONE-v1.2] provided a certain threshold of deformed curve points, de-curve them - - [DONE-v1.3] provide as optional - [DONE-v1.6] optimize algorithm by only applying the nodes as a last, final step (since that process takes the longest) instead of during the refinement and deformation stages - final pass of code optimization and cleanup */ var dom = fw.getDocumentDOM(); var sel = [].concat(fw.selection); function DeformToPath() { // require active document if (!dom) return false; // validate selection var paths = []; function pathsIn(elems){ for(var e in elems){ if(elems[e] == '[object Path]') paths.push(elems[e]); if(elems[e] == '[object Group]') pathsIn(elems[e].elements) } } pathsIn(sel); if(paths.length < 2) return alert('This command requires at least one selected path to be deformed, and a top path to deform to.'); if(paths[0].contours.length != 1) return alert('The top path to deform to must have exactly one contour.'); if(paths[0].contours[0].nodes.length < 2) return alert('The top path to deform to must have at least two points.'); // user input var MAX_THRESHOLD = 180, MIN_THRESHOLD = 5; var input; do{ input = prompt("Conformity threshold (" + MIN_THRESHOLD + "-" + MAX_THRESHOLD + "; lower number for higher conformity, cancel to not add points to conform to contour):", fw.DeformToPath_input || 25); }while(!validate()); if(input == null) return false; input = fw.DeformToPath_input = Math.min(Math.max(Number(input), MIN_THRESHOLD), MAX_THRESHOLD); function validate(){ if(isNaN(Number(input))) return alert('Invalid input! Enter numeric values from ' + MIN_THRESHOLD + ' to ' + MAX_THRESHOLD + ' only.'); return true; } // remove top most path and make it the deformer contour var deformPath = paths.shift(); // determine likely orientation fw.selection = paths; var bounds = dom.getSelectionBounds(); if(bounds.right - bounds.left > bounds.bottom - bounds.top){ dom.rotateSelection(-90, "autoTrimImages transformAttributes"); // reconstruct paths, cause rotating destroys all the references paths = []; for(var s in fw.selection){ if(fw.selection[s] == '[object Path]') paths.push(fw.selection[s]); } } // first we need to measure the deform contour so we can map by its length // we approximate this by walking the beziers at small intervals and measuring distance // we also track the angle change to determine at what distances along the curve we will need to add points to a path to maintain conformity var isClosed = deformPath.contours[0].isClosed; var deformDistance = 0, prevDistance = 0, pt, ptA, ptB, bez; var ln = deformPath.contours[0].nodes.length - (isClosed ? 0 : 1); var BEZIER_STEPS = 100, prevPoint = deformPath.contours[0].nodes[0]; var bezPoints = [{x:prevPoint.x, y:prevPoint.y, time:0, distance:0}], beziers = []; var conformDistances = [], ADD_POINT_ANGLE_THRESHOLLD = input/*45*/, changedAngle = 0, dif; for(var n = 0; n < ln; n++){ ptA = deformPath.contours[0].nodes[n]; ptB = (isClosed && n == ln - 1) ? deformPath.contours[0].nodes[0] : deformPath.contours[0].nodes[n+1]; bez = {p2:{x:ptA.x, y:ptA.y}, cp2:{x:ptA.succX, y:ptA.succY}, cp1:{x:ptB.predX, y:ptB.predY}, p1:{x:ptB.x, y:ptB.y}}; //bez = {p1:{x:ptA.x,y:ptA.y},cp1:{x:ptA.succX,y:ptA.succY},cp2:{x:ptB.predX,y:ptB.predY},p2:{x:ptB.x,y:ptB.y}}; //prevPoint = ptA; for(var i = 1; i <= BEZIER_STEPS; i++){ pt = getBezier(i / BEZIER_STEPS, bez.p1, bez.cp1, bez.cp2, bez.p2); // TODO: optimize redundent vector calculation deformDistance += getDistance(prevPoint,pt); pt.angle = getAngle(prevPoint,pt); if(/*input!=null &&*/ prevPoint.angle!=undefined){ dif = prevPoint.angle - pt.angle; //dom.addNewOval({left:pt.x-2,right:pt.x+2,top:pt.y-2,bottom:pt.y+2}); if(Math.abs(dif) > 180) dif -= 360 * (dif > 0 ? 1 : -1) if((changedAngle += Math.abs(dif)) > ADD_POINT_ANGLE_THRESHOLLD){ while(changedAngle > ADD_POINT_ANGLE_THRESHOLLD) changedAngle -= ADD_POINT_ANGLE_THRESHOLLD;// * (changedAngle > 0 ? 1 : -1); conformDistances.push(deformDistance); } } prevPoint = pt; bezPoints.push({x:pt.x, y:pt.y, t:i, distance:deformDistance}); // not used yet, could be used for optimization below... } bez = {p1:{x:ptA.x, y:ptA.y}, cp1:{x:ptA.succX, y:ptA.succY}, cp2:{x:ptB.predX, y:ptB.predY} ,p2:{x:ptB.x, y:ptB.y}}; // flip bezier beziers.push({bez:bez, distance:deformDistance - prevDistance}); prevDistance = deformDistance; } //alert(deformDistance); // convert add point distances to relative time value of 0-1 for(var a = 0; a < conformDistances.length; a++) conformDistances[a] = conformDistances[a] / deformDistance; // determine bounding box of all nodes in the paths, including handle positions // we'll use this to map relative node positions from the original bounds to the deform contour var pts = [], node; for(var p = 0; p < paths.length; p++){ for(var c = 0; c < paths[p].contours.length; c++){ for(var n = 0; n < paths[p].contours[c].nodes.length; n++){ node = paths[p].contours[c].nodes[n]; pts.push({x:node.x, y:node.y}, {x:node.succX, y:node.succY}, {x:node.predX, y:node.predY}); } } } var xSorted = [].concat(pts).sort(function(a,b){return a.x - b.x}); var ySorted = [].concat(pts).sort(function(a,b){return a.y - b.y}); var bounds = { left:xSorted[0].x, top:ySorted[0].y, right:xSorted[xSorted.length - 1].x, bottom:ySorted[ySorted.length - 1].y } var center = bounds.left + ((bounds.right - bounds.left) / 2), top = bounds.top, height = bounds.bottom - bounds.top; // add points to original paths based on where they intersect a normal along the deform path we found to exceed the conformity threshold var newPaths = [].concat(paths); var Y, node, nextNode, pt, bez, nodes, newNodes, conformedNodes, nn, intersections = 0; for(var p = 0; p < paths.length; p++){ // create new path data newPaths[p] = {contours:[]}; for(var c = 0; c < paths[p].contours.length; c++){ // create new contour for the new path nodes = paths[p].contours[c].nodes; newPaths[p].contours[c] = {nodes:[]}; // add all the existing points to new contour newNodes = newPaths[p].contours[c].nodes; for(var n = 0; n < nodes.length; n++) newNodes.push(copyNode(nodes[n])); // now find and add any points necessary to conform to the deform contour for(var a = 0; a < conformDistances.length; a++){ // interpolate current conform distance to y coordinate within path bounds Y = bounds.bottom - height * conformDistances[a]; // construct the contour again including new nodes for any intersecting Y points on the bezier pairs for current conform distance conformedNodes = []; for(var n = 0; n < newNodes.length; n++){ // maintain current node on contour conformedNodes.push(newNodes[n]); if(n == newNodes.length - 1 && !paths[p].contours[c].isClosed) break; node = newNodes[n]; // bezier segment node pair nextNode = n == newNodes.length - 1 ? conformedNodes[0] : newNodes[n + 1]; prevNode = conformedNodes[conformedNodes.length - 1]; // find bezier segment y intersections, there can not be more than 3 for a single bezier segment intersections = 0; while(intersections++ < 3 && checkBezierIntersection(prevNode, nextNode, Y)){ // find the Y intersecting point along bezier curve, if there is one pt = getBezierAtY(Y, prevNode, {x:prevNode.succX, y:prevNode.succY}, {x:nextNode.predX, y:nextNode.predY}, nextNode); if(!pt) break; // if it intersects, split the bezier into two new bezier segments bez = splitBezier(pt, prevNode, {x:prevNode.succX, y:prevNode.succY}, {x:nextNode.predX, y:nextNode.predY}, nextNode); conformedNodes.push(copyNode(newNodes[n])); nn = conformedNodes[conformedNodes.length - 1]; nn.x = bez.p3.x; nn.y = bez.p3.y; nn.predX = bez.cp3.x; nn.predY = bez.cp3.y; nn.succX = bez.cp4.x; nn.succY = bez.cp4.y; nn.isCurvePoint = true; prevNode.succX = bez.cp1.x; prevNode.succY = bez.cp1.y; nextNode.predX = bez.cp2.x; nextNode.predY = bez.cp2.y; prevNode = conformedNodes[conformedNodes.length - 1]; } } // commit to newly conformed nodes for(var n = 0; n < conformedNodes.length; n++) newNodes[n] = copyNode(conformedNodes[n]); } } } // map all nodes relative to the bouding box var nodes = [], nod, hasPred, hasSucc, ln, SMOOTH_HANDLE_PERCENT = .35; for(var p = 0; p < newPaths.length; p++){ for(var c = 0; c < newPaths[p].contours.length; c++){ ln = newPaths[p].contours[c].nodes.length; for(var n = 0; n < ln; n++){ nod = newPaths[p].contours[c].nodes[n]; hasPred = (nod.isCurvePoint || nod.predX!=nod.x || nod.predY!=nod.y); hasSucc = (nod.isCurvePoint || nod.succX!=nod.x || nod.succY!=nod.y); nodes.push({p:p, c:c, n:n, type:'node', x:nod.x - center, y:1 - (nod.y - top) / height, hasPred:true/*hasPred*/, hasSucc:true/*hasSucc*/}); // if node-to-prevNode is a line, make it smooth prevNode = n == 0 ? newPaths[p].contours[c].nodes[ln - 1] : newPaths[p].contours[c].nodes[n - 1]; if(!hasPred && prevNode.succX == prevNode.x && prevNode.succY == prevNode.y){ nod.predX = nod.x + (prevNode.succX-nod.x) * SMOOTH_HANDLE_PERCENT; nod.predY = nod.y + (prevNode.succY-nod.y) * SMOOTH_HANDLE_PERCENT; prevNode.succX = prevNode.x + (nod.x - prevNode.succX) * SMOOTH_HANDLE_PERCENT; prevNode.succY = prevNode.y + (nod.y - prevNode.succY) * SMOOTH_HANDLE_PERCENT; } // if node-to-nextNode is a line, make smooth nextNode = n == ln-1 ? newPaths[p].contours[c].nodes[0] : newPaths[p].contours[c].nodes[n + 1]; if(!hasSucc && nextNode.predX == nextNode.x && nextNode.predY == nextNode.y){ nod.succX = nod.x + (nextNode.predX - nod.x) * SMOOTH_HANDLE_PERCENT; nod.succY = nod.y + (nextNode.predY - nod.y) * SMOOTH_HANDLE_PERCENT; nextNode.predX = nextNode.x + (nod.x - nextNode.predX) * SMOOTH_HANDLE_PERCENT; nextNode.predY = nextNode.y + (nod.y - nextNode.predY) * SMOOTH_HANDLE_PERCENT; } //if(hasPred) nodes.push({p:p, c:c, n:n, type:'pred', x:nod.predX - center, y:1 - (nod.predY - top) / height}); //if(hasSucc) nodes.push({p:p, c:c, n:n, type:'succ', x:nod.succX - center, y:1 - (nod.succY - top) / height}); } } } // sort mapped nodes by Y position, so we can map them sequentially when walking the deformPath contour function byY(a,b){ return a.y - b.y; } nodes.sort(byY); // now map all paths points to the deformPath var ln = nodes.length; var nod, yDist, dist, bez, pt, isNode, vector, origNode, xvar, yvar; for(var n = 0; n < ln; n++){ nod = nodes[n]; yDist = deformDistance * nod.y; dist = 0; for(var b = 0; b < beziers.length; b++){ dist += beziers[b].distance; if(dist >= yDist) break; } if(b == beziers.length) b--; bez = beziers[b].bez; pt = getBezierAtDistance(dist - yDist, bez.p1, bez.cp1, bez.cp2, bez.p2); // optimizable by using bezPoints as starting point isNode = nod.type == 'node'; xvar = isNode ? 'x' : nod.type + 'X'; yvar = isNode ? 'y' : nod.type + 'Y'; vector = getVector(nod.x, (pt.angle + 90)); origNode = newPaths[nod.p].contours[nod.c].nodes[nod.n]; origNode[xvar] = pt.x + vector.x; origNode[yvar] = pt.y + vector.y; if(isNode && !nod.hasPred){ origNode.predX = pt.x + vector.x; origNode.predY = pt.y + vector.y; } if(isNode && !nod.hasSucc){ origNode.succX = pt.x + vector.x; origNode.succY = pt.y + vector.y; } } // finally, walk the now-deformed paths and smooth out any curve points var nod, predAngle, succAngle, avgAngle, dif, dist, vector, MAX_CURVE_ANGLE = 45; for(var p = 0; p < newPaths.length; p++){ for(var c = 0; c < newPaths[p].contours.length; c++){ var nlen = newPaths[p].contours[c].nodes.length; for(var n = 0; n < nlen; n++){ nod = newPaths[p].contours[c].nodes[n]; if(nod.isCurvePoint){ // find average handle tangent predAngle = getAngle({x:nod.predX, y:nod.predY},nod); succAngle = getAngle(nod, {x:nod.succX, y:nod.succY}); dif = predAngle - succAngle; avgAngle = succAngle + dif / 2; if(Math.abs(dif) > 180) avgAngle += 180; // determine if situation is right for smoothing if(predAngle == avgAngle || succAngle == avgAngle) continue; if(Math.abs(Math.abs(avgAngle) - Math.abs(predAngle)) > MAX_CURVE_ANGLE){ nod.isCurvePoint = false; continue; } // adjust successor handle to average dist = getDistance(nod, {x:nod.succX, y:nod.succY}); vector = getVector(dist, avgAngle); nod.succX = nod.x + vector.x; nod.succY = nod.y + vector.y; // adjust predecessor handle to average dist = getDistance(nod, {x:nod.predX, y:nod.predY}); vector = getVector(dist, avgAngle + 180); nod.predX = nod.x + vector.x; nod.predY = nod.y + vector.y; } } } } // finally, apply all the now deformed nodes var nodes, isClosed; for(var p = 0; p < newPaths.length; p++){ for(var c = 0; c < newPaths[p].contours.length; c++){ nodes = newPaths[p].contours[c].nodes; isClosed = paths[p].contours[c].isClosed; paths[p].contours[c] = new Contour(); paths[p].contours[c].isClosed = isClosed; var nlen = nodes.length; for(var n = 0; n < nlen; n++){ paths[p].contours[c].nodes[n] = new ContourNode(); pasteNode(paths[p].contours[c].nodes[n], nodes[n]); } } if(paths[p].contours.length == 1) paths[p].isEvenOddFill = false; } } //try{ DeformToPath(); //}catch(e){ alert([e, e.lineNumber, e.fileName].join("\n")) }; // find the distance between two points function getDistance(p1, p2){ return Math.sqrt(((p1.x - p2.x) * (p1.x - p2.x)) + ((p1.y - p2.y) * (p1.y - p2.y))); } // get the angle in degrees between two points function getAngle(p1,p2){ return -Math.atan2((p1.x - p2.x), (p1.y - p2.y)) * (180 / Math.PI); } function getAngleRadians(p1, p2){ return -Math.atan2((p1.x - p2.x), (p1.y - p2.y)); } // given angle and distance, find x,y translation function getVector(distance, angle){ var xS = distance * Math.sin(angle * (Math.PI / 180)); var yS = -distance * Math.cos(angle * (Math.PI / 180)); return {x:xS, y:yS}; } // copy node function copyNode(pt){ var ptCopy = {} copyProps(ptCopy, pt, ["x", "y", "succX", "succY", "predX", "predY", "randomSeed", "isSelectedPoint", "isCurvePoint", "name"]); ptCopy.dynamicInfo = []; for(var i = 0; i < pt.dynamicInfo.length; i++) ptCopy.dynamicInfo[i] = copyProps({}, pt.dynamicInfo[i], ["pressure", "duration", "velocity"]); return ptCopy; //return eval("(" + pt.toSource() + ")"); } // paste node function pasteNode(pt, ptCopy){ copyProps(pt, ptCopy, ["x", "y", "succX", "succY", "predX", "predY", "randomSeed", "isSelectedPoint", "isCurvePoint", "name"]); var dynamicInfo = []; for(var i = 0; i < ptCopy.dynamicInfo.length; i++) dynamicInfo.push(copyProps(new ContourNodeDynamicInfo(), ptCopy.dynamicInfo[i], ["pressure", "duration", "velocity"])); pt.dynamicInfo = dynamicInfo; return pt; } // copy props by name function copyProps(targetObj, sourceObj, props){ var p = props.length; while(p--) targetObj[props[p]] = sourceObj[props[p]]; return targetObj; } // find a point along a bezier segment(as defined by p1, cp1, cp2, p2) at a specified percent(0-100) function getBezier(percent, p1, cp1, cp2, p2) { function b1(t) { return t*t*t } function b2(t) { return 3*t*t*(1-t) } function b3(t) { return 3*t*(1-t)*(1-t) } function b4(t) { return (1-t)*(1-t)*(1-t) } var pos = {x:0, y:0}; pos.x = p1.x * b1(percent) + cp1.x * b2(percent) + cp2.x * b3(percent) + p2.x * b4(percent); pos.y = p1.y * b1(percent) + cp1.y * b2(percent) + cp2.y * b3(percent) + p2.y * b4(percent); return pos; } // check for possible, though not definite, bezier intersection function checkBezierIntersection(node, nextNode, y){ if((node.y > y && nextNode.y < y) || (node.y < y && nextNode.y > y)) return true; return !(node.y < y && node.succY < y && nextNode.predY < y && nextNode.y < y) && !(node.y > y && node.succY > y && nextNode.predY > y && nextNode.y > y); } // find the point along a bezier segment(as defined by p1, cp1, cp2, p2) at a specified distance from p1 function getBezierAtDistance(distance, p1, cp1, cp2, p2, steps, iterations){ if(!steps) var steps = 10; if(!iterations) var iterations = 5; var i, dist, currDist, prevPoint, point, prevPrevPoint, currPercent, DIF_TOLERANCE = 0.5; return doWalkBezier(0, 1); function doWalkBezier(minPercent, maxPercent, startDistance){ dist = startDistance || 0; var rangePercent = maxPercent - minPercent, step = 1 / steps; for(var i = 0; i <= steps; i++){ currPercent = minPercent + rangePercent * (step * i); point = getBezier(currPercent, p1, cp1, cp2, p2); point.percent = currPercent; if(prevPoint){ dist += currDist = getDistance(point, prevPoint); if(dist > distance){ /*if(dist - distance < DIF_TOLERANCE){ point.angle = getAngleRadians(point, prevPoint); return returnPoint(point); }*/ prevPoint.angle = getAngleRadians(point, prevPoint); return --iterations ? doWalkBezier(prevPoint.percent, point.percent, dist - currDist) : returnPoint(prevPoint); } } prevPrevPoint = prevPoint; prevPoint = point; } point.angle = getAngleRadians(point, prevPrevPoint); return returnPoint(point); } function returnPoint(p){ p.angle *= 180 / Math.PI; // radians to degrees return p; } } // find the first point that intersects Y function getBezierAtY(y, p1, cp1, cp2, p2, steps, iterations){ if(!steps) var steps = 10; if(!iterations) var iterations = 5; var i, prevDif, prevPrevPoint, currDif, dif, prevPoint, point, currPercent, DIF_TOLERANCE = 0.5; return doWalkBezier(0, 1); function doWalkBezier(minPercent, maxPercent){ var rangePercent = maxPercent - minPercent, step = 1 / steps; for(var i = 0; i <= steps; i++){ currPercent = minPercent + rangePercent *(step * i); point = getBezier(currPercent, p2, cp2, cp1, p1); point.percent = currPercent; point.dif = point.y - y; if(prevPoint){ if(Math.abs(point.dif) < DIF_TOLERANCE){ return point; }else if((point.dif > 0 && prevPoint.dif < 0) || (point.dif < 0 && prevPoint.dif > 0)){//point.dif > prevPoint.dif && point.x != p1.x){ if(prevPrevPoint) prevPoint = prevPrevPoint; else prevPrevPoint = prevPoint; return --iterations ? doWalkBezier(prevPrevPoint.percent, point.percent) : point; } }else{ if(Math.abs(point.dif) < DIF_TOLERANCE) point.dif = 0; // start point } prevPrevPoint = prevPoint; prevPoint = point; } return null;//prevPoint; } } // splits a bezier segment(as defined by p1, cp1, cp2, p2) at a specified point function splitBezier(pt, p1, cp1, cp2, p2){ var P1 = p1;//{x:p1.x, y:p1.y}; var C1 = cp1;//{x:p1.succX, y:p1.succY}; var C2 = cp2;//{x:p2.predX, y:p2.predY}; var P2 = p2;//{x:p2.x, y:p2.y}; var P3 = getBezier(pt.percent, P2, C2, C1, P1)//pt;//getBezierAtDistance(distance,P2,C2,C1,P1); var t3 = pt.percent;//P3.percent; var U = {x: (C1.x - P1.x)*t3,y: (C1.y - P1.y)*t3} var V = {x: ((C2.x - C1.x)*t3 - U.x)*t3,y: ((C2.y - C1.y)*t3 - U.y)*t3} var newC1 = {x: U.x + P1.x,y: U.y + P1.y} var C3 = {x: U.x + V.x + newC1.x,y: U.y + V.y + newC1.y} var R = {x: (C2.x - P2.x)*(1-t3),y: (C2.y - P2.y)*(1-t3)} var S = {x: ((C1.x - C2.x)*(1-t3) - R.x)*(1-t3),y: ((C1.y - C2.y)*(1-t3) - R.y)*(1-t3)} var newC2 = {x: R.x + P2.x,y: R.y + P2.y} var C4 = {x: R.x + S.x + newC2.x,y: R.y + S.y + newC2.y} //return {C4:C4,C3:C3,t3:t3,P3:P3}; return {time:t3, cp1:newC1, cp2:newC2, p3:P3, cp3:C3, cp4:C4}; }